﻿using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleAppFramework.Util
{
    //https://blog.csdn.net/farway000/article/details/131820729?spm=1001.2014.3001.5501
    public sealed class Batch<T> : IEnumerable<T>, IDisposable where T : class
    {
        private bool _isDisposed;
        private int? _count;
        private readonly T[] _data;
        private static readonly ArrayPool<T> _pool
            = ArrayPool<T>.Create();

        public int Count
        {
            get
            {
                if (_isDisposed)
                    throw new ObjectDisposedException(nameof(Batch<T>));
                if (_count.HasValue)
                    return _count.Value;
                var count = 0;
                for (int index = 0; index < _data.Length; index++)
                {
                    if (_data[index] is null)
                    {
                        break;
                    }
                    count++;
                }
                return (_count = count).Value;
            }
        }
        public Batch(T[] data)
           => _data = data
               ?? throw new ArgumentNullException(nameof(data));
        public void Dispose()
        {
            _pool.Return(_data, clearArray: true);
            _isDisposed = true;
        }
        public IEnumerator<T> GetEnumerator()
           => new Enumerator(this);
        IEnumerator IEnumerable.GetEnumerator()
           => GetEnumerator();
        public static T[] CreatePooledArray(int batchSize)
           => _pool.Rent(batchSize);
        private void EnsureNotDisposed()
        {
            if (_isDisposed)
                throw new ObjectDisposedException(nameof(Batch<T>));
        }

        private sealed class Enumerator : IEnumerator<T>
        {
            private readonly Batch<T> _batch;
            private readonly T[] _data;
            private int _index = -1;
            public Enumerator(Batch<T> batch)
            {
                _batch = batch;
                _data = batch._data;
            }
            public T Current
            {
                get
                {
                    _batch.EnsureNotDisposed();
                    return _data[_index];
                }
            }
            object IEnumerator.Current => Current;
            public void Dispose() { }
            public bool MoveNext()
            {
                _batch.EnsureNotDisposed();
                return ++_index < _data.Length
                   && _data[_index] is not null;
            }
            public void Reset()
            {
                _batch.EnsureNotDisposed();
                _index = -1;
            }
        }
    }


    public sealed class Batcher<T> where T : class
    {
        private readonly int _interval;
        private readonly int _batchSize;
        private readonly Action<Batch<T>> _processor;
        private volatile Container _container;
        private readonly Timer _timer;
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

        public Batcher(
            Action<Batch<T>> processor,
            int batchSize,
            TimeSpan interval)
        {
            _interval = (int)interval.TotalMilliseconds;
            _batchSize = batchSize;
            _processor = processor;
            _container = new Container(batchSize);
            _timer = new Timer(_ => Process(), null, _interval, Timeout.Infinite);
        }

        private void Process()
        {
            if (_container.IsEmpty) return;
            _lock.EnterWriteLock();
            try
            {
                if (_container.IsEmpty) return;
                var container = Interlocked.Exchange(
                    ref _container, new Container(_batchSize));
                _ = Task.Run(() => _processor(container.AsBatch()));
                _timer.Change(_interval, Timeout.Infinite);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        public void Add(T item)
        {
            _lock.EnterReadLock();
            bool success = false;
            try
            {
                success = _container.TryAdd(item);
            }
            finally
            {
                _lock.ExitReadLock();
            }

            if (!success)
            {
                Process();
                new SpinWait().SpinOnce();
                Add(item);
            }
        }

        private sealed class Container
        {
            private volatile int _next = -1;
            private readonly T[] _data;
            public bool IsEmpty
                => _next == -1;
            public Container(int batchSize)
                => _data = Batch<T>.CreatePooledArray(batchSize);
            public bool TryAdd(T item)
            {
                var index = Interlocked.Increment(ref _next);
                if (index > _data.Length - 1) return false;
                _data[index] = item;
                return true;
            }
            public Batch<T> AsBatch() => new Batch<T>(_data);
        }
    }
}
